<?php
/**
 * 实例化开放平台的 EasyWechat App
 *
 * Author: Zeon
 * Date: 2017/7/21
 * Time: 16:24
 * Created by PhpStorm.
 */
namespace App\Services\OpenWechat;


use App\Exceptions\ApiErrCode;
use App\Exceptions\ApiException;
use App\Http\Controllers\OpenWechat\OAuthController;
use App\Services\OpenWechat\Debug\AuthorizerAccessToken;
use EasyWeChat\Factory;
use EasyWeChat\Kernel\Support\Collection;
use EasyWeChat\OpenPlatform\Auth\AccessToken;
use EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Application;
use Psr\SimpleCache\InvalidArgumentException;
use Symfony\Component\Cache\Simple\RedisCache;

class AppFactory
{
    const ERROR_CODE_API_DAILY_MAX = 45009; // open wechat reach max api daily quota limit
    protected $clear_quota_res = null;

    /** @var \EasyWeChat\OpenPlatform\Application  */
    protected $open_wx;

    /**
     * AppFactory constructor.
     */
    public function __construct()
    {
        $this->open_wx = Factory::openPlatform($this->getConfig());

        if (config('pkx.open_wechat.cache_driver')) {
            /**
             * 使用 redis 共享缓存，不填写默认是 filesystem 缓存
             *
             * @link https://www.easywechat.com/docs/master/customize/cache
             */
            $this->open_wx['cache'] = $this->getCacheDriver();;
        }
    }

    /**
     * @return array
     */
    private function getConfig()
    {
        /**
         * @link https://www.easywechat.com/docs/master/official-account/configuration - 参数文档
         */
        $config = [
            'app_id'  => config('pkx.open_wechat.app_id'),
            'secret'  => config('pkx.open_wechat.secret'),
            'token'   => config('pkx.open_wechat.token'),
            'aes_key' => config('pkx.open_wechat.aes_key'),

            'debug' => config('app.debug') || config('pkx.open_wechat.debug'),

            'log' => [
                'level'  => 'error',
                'driver' => 'daily',
                'path'   => storage_path('logs/easy-wechat.log'),
            ],

            'oauth' => [
                'callback' => route(OAuthController::OAUTH_CALL_BACK_ROUTE)
            ],

            // 线上出现过  cURL error 7: Failed to connect to api.weixin.qq.com port 443: Operation timed out
            // 设置 guzzle 的配置，超时与重试次数
            'http' => [
                'max_retries' => 3,
                'retry_delay' => 500,
                'timeout' => 5.0,
            ],
        ];

        return $config;
    }

    /**
     * @return \EasyWeChat\OpenPlatform\Application
     */
    public static function openWechat()
    {
        return (new self)->open_wx;
    }
    
    /**
     * return easy wechat authroizer app
     *
     * @param string $appid
     *
     * @return Application
     */
    public function authorizerApp(string $appid): Application
    {
        $is_dev = config('app.debug')
            && config(AuthorizerAccessToken::ONLINE_ACS_TOKEN_API_CONF_KEY);
        
        $refresh_token = $is_dev ? null : $this->getAuthorRefreshToken($appid);

        $app = $this->open_wx->officialAccount($appid, $refresh_token);

        if ($is_dev) {
            $this->useOnlineAuthorToken($app, $appid);
        }

        return $app;
    }

    /**
     * @param $appid
     * @return mixed
     */
    public function authInfo($appid)
    {
        return new Collection($this->open_wx->getAuthorizer($appid));
    }

    /**
     *
     * Static init
     * @param $appid
     * @return \EasyWeChat\OfficialAccount\Server\Guard
     */
    public function authorizerServer($appid)
    {
        $app = (new self())->authorizerApp($appid);

        return $app->server;
    }
    
    /**
     * 本地开发时自动请求设置线上的 token
     *
     * @param Application $app
     * @param string      $appid
     */
    private function useOnlineAuthorToken(Application $app, string $appid): void
    {
        /**
         * @link https://www.easywechat.com/docs/master/customize/access_token
         */
        $online_token = AuthorizerAccessToken::get($appid);
        if ($online_token) {
            /** @var AccessToken $accessToken */
            $accessToken = $app['access_token'];
            
            try {
                $accessToken->setToken($online_token, AuthorizerAccessToken::TOKEN_CACHE_MINUTE * 60);
            } catch (InvalidArgumentException $e) {
                throw new ApiException(ApiErrCode::UNKNOWN_ERROR, $e->getMessage());
            }
        }
    }

    /**
     * 线上每次都重新获取 refresh token
     *
     * @param $appid
     *
     * @return mixed
     */
    private function getAuthorRefreshToken(string $appid): string
    {
        /**
         * @link https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1453779503&token=&lang=zh_CN
         * 微信的开放平台获取公众号信息文档 - api_get_authorizer_info
         *
         * @uses AccessToken::getCredentials - 本地调试，示例如下：
         *  1. 请修改 component_verify_ticket 为线上数据库 component_tokens 表中的值
         *  2. 替换 getCredentials 方法中的代码如下：
         *    'component_verify_ticket' => \App\Models\OpenWechat\ComponentToken::first()->component_verify_ticket
         */
        $auth_info = $this->authInfo($appid);

        // 检查是否超出当日请求数量，需要请求清除限制
        if ($auth_info->errcode === self::ERROR_CODE_API_DAILY_MAX && ! $this->clear_quota_res) {
            $this->clear_quota_res = $this->open_wx->clearQuota();
            return $this->getAuthorRefreshToken($appid);
        }

        $refresh_token = $auth_info->authorization_info['authorizer_refresh_token'] ?? null;
        if (! $refresh_token) {
            logger($auth_info);
            throw new ApiException(ApiErrCode::UNKNOWN_ERROR, 'wx authorizer refresh token request failed');
        }

        return $refresh_token;
    }

    private function getCacheDriver(): RedisCache
    {
        $redis_client = app('redis')
            ->connection()
            ->client();

        $cache = new RedisCache($redis_client);

        return $cache;
    }
}